#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import http.server
import socketserver
import json
import os
import webbrowser
import requests
from datetime import datetime, timedelta
import time
from typing import List, Dict, Any
import re

# 导入高德API
from backend.gaode_api import GaodeAPI

# 12306 实时查询配置
DEFAULT_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Referer": "https://kyfw.12306.cn/otn/leftTicket/init",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Connection": "keep-alive",
}
QUERY_ENDPOINTS = [
    "https://kyfw.12306.cn/otn/leftTicket/query",
    "https://kyfw.12306.cn/otn/leftTicket/queryZ",
    "https://kyfw.12306.cn/otn/leftTicket/queryA",
    "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice",
]

# 简单内存缓存
RAIL_CACHE: Dict[str, Any] = {}
PRICE_CACHE: Dict[str, Any] = {}

# 高德API配置
GAODE_API_KEY = "003df4ac18f1cca97eaf59dc90afbaa2"
_gaode_api = None

def get_gaode_api():
    global _gaode_api
    if _gaode_api is None:
        _gaode_api = GaodeAPI(GAODE_API_KEY)
    return _gaode_api

# 聚合数据API配置（航班）
JUHE_FLIGHT_APPKEY = "b0daee5c7cf963ae6458020601583575"

# 城市到IATA机场代码映射
CITY_TO_IATA = {
    '大连': 'DLC', '北京': 'PEK', '上海': 'PVG', '广州': 'CAN',
    '深圳': 'SZX', '成都': 'CTU', '重庆': 'CKG', '西安': 'XIY',
    '武汉': 'WUH', '南京': 'NKG', '杭州': 'HGH', '天津': 'TSN',
    '青岛': 'TAO', '厦门': 'XMN', '长沙': 'CSX', '郑州': 'CGO',
    '合肥': 'HFE', '济南': 'TNA', '福州': 'FOC', '南昌': 'KHN',
    '石家庄': 'SJW', '太原': 'TYN', '呼和浩特': 'HET', '沈阳': 'SHE',
    '长春': 'CGQ', '哈尔滨': 'HRB', '昆明': 'KMG', '贵阳': 'KWE',
    '南宁': 'NNG', '海口': 'HAK', '三亚': 'SYX', '兰州': 'LHW',
    '西宁': 'XNN', '银川': 'INC', '乌鲁木齐': 'URC', '拉萨': 'LXA'
}

# 周边城市映射（用于前端下拉与联运）
NEARBY_CITIES = {
    '大连': ['沈阳', '营口', '丹东', '鞍山'],
    '合肥': ['南京', '武汉', '郑州', '济南'],
    '北京': ['天津', '石家庄', '保定', '廊坊'],
    '上海': ['南京', '杭州', '苏州', '无锡'],
    '广州': ['深圳', '佛山', '东莞', '中山'],
    '深圳': ['广州', '东莞', '惠州', '佛山']
}

# 常用枢纽优先级（城市 -> 机场名/车站名列表）
CITY_MAJOR_AIRPORTS = {
    '南京': ['南京禄口国际机场', '南京禄口机场', '南京机场'],
    '合肥': ['合肥新桥国际机场', '合肥新桥机场', '合肥机场'],
    '大连': ['大连周水子国际机场', '大连周水子机场', '大连机场'],
    '北京': ['北京首都国际机场', '北京大兴国际机场', '首都机场', '大兴机场'],
}
CITY_MAJOR_STATIONS = {
    '南京': ['南京南站', '南京站'],
    '合肥': ['合肥南站', '合肥站'],
    '大连': ['大连北站', '大连站'],
    '北京': ['北京南站', '北京西站', '北京站', '清河站', '北京朝阳站'],
}

# 规模与衔接配置
STATION_SCALE = {
    '南京南站': 'mega', '南京站': 'large',
    '合肥南站': 'large', '合肥站': 'medium',
    '上海虹桥站': 'mega', '北京南站': 'mega',
}
AIRPORT_SCALE = {
    '南京禄口国际机场': 'hub', '合肥新桥国际机场': 'large',
    '上海虹桥国际机场': 'hub', '北京首都国际机场': 'hub'
}
# 省会枢纽优先映射（可按需补充），用于无直达时的回退
PROVINCE_CAPITAL_HUB = {
   '北京': {'city': '北京', 'iata': 'PEK'},
   '天津': {'city': '天津', 'iata': 'TSN'},
   '上海': {'city': '上海', 'iata': 'PVG'},
   '重庆': {'city': '重庆', 'iata': 'CKG'},
   '辽宁': {'city': '沈阳', 'iata': 'SHE'},
   '吉林': {'city': '长春', 'iata': 'CGQ'},
   '黑龙江': {'city': '哈尔滨', 'iata': 'HRB'},
   '河北': {'city': '石家庄', 'iata': 'SJW'},
   '山西': {'city': '太原', 'iata': 'TYN'},
   '内蒙古': {'city': '呼和浩特', 'iata': 'HET'},
   '江苏': {'city': '南京', 'iata': 'NKG'},
   '浙江': {'city': '杭州', 'iata': 'HGH'},
   '安徽': {'city': '合肥', 'iata': 'HFE'},
   '福建': {'city': '福州', 'iata': 'FOC'},
   '江西': {'city': '南昌', 'iata': 'KHN'},
   '山东': {'city': '济南', 'iata': 'TNA'},
   '河南': {'city': '郑州', 'iata': 'CGO'},
   '湖北': {'city': '武汉', 'iata': 'WUH'},
   '湖南': {'city': '长沙', 'iata': 'CSX'},
   '广东': {'city': '广州', 'iata': 'CAN'},
   '广西': {'city': '南宁', 'iata': 'NNG'},
   '海南': {'city': '海口', 'iata': 'HAK'},
   '四川': {'city': '成都', 'iata': 'CTU'},
   '贵州': {'city': '贵阳', 'iata': 'KWE'},
   '云南': {'city': '昆明', 'iata': 'KMG'},
   '西藏': {'city': '拉萨', 'iata': 'LXA'},
   '陕西': {'city': '西安', 'iata': 'XIY'},
   '甘肃': {'city': '兰州', 'iata': 'LHW'},
   '青海': {'city': '西宁', 'iata': 'XNN'},
   '宁夏': {'city': '银川', 'iata': 'INC'},
   '新疆': {'city': '乌鲁木齐', 'iata': 'URC'},
}
# 跨省优选枢纽（根据经验补充，如安徽常经由南京中转）
PROVINCE_ADJACENT_HUBS = {
    '安徽': ['南京'],
}
CONNECTION_BUFFER_RULES = {
    'flight_to_rail': {
        'hub':    {'driving': 50, 'transit': 65},
        'large':  {'driving': 45, 'transit': 60},
        'medium': {'driving': 40, 'transit': 55},
        'small':  {'driving': 35, 'transit': 50},
    },
    'rail_to_rail': {
        'mega':   35,
        'large':  25,
        'medium': 20,
        'small':  15,
    },
    'rail_to_flight': {
        'hub':    {'driving': 70, 'transit': 85},
        'large':  {'driving': 60, 'transit': 80},
        'medium': {'driving': 55, 'transit': 75},
        'small':  {'driving': 50, 'transit': 70},
    }
}
HOUR_CONGESTION_MULTIPLIER = {
    '07': 1.2, '08': 1.3, '17': 1.3, '18': 1.25
}
DEFAULT_STATION_SCALE = 'large'
DEFAULT_AIRPORT_SCALE = 'large'

# 目的地城市无法直达时的邻近城市回退（用于联运）
DEST_CITY_FALLBACKS = {
    '东至': ['池州', '安庆', '铜陵', '宣城', '芜湖'],
    '东至县': ['池州', '安庆', '铜陵', '宣城', '芜湖'],
    '池州': ['安庆', '铜陵', '芜湖', '宣城', '合肥'],
}

# 基础工具

def _norm_city(name: str) -> str:
    if not name:
        return ''
    n = str(name).strip()
    if '(' in n:
        n = n.split('(')[0]
    # 常见机构/站点到城市映射（与 liudang 同步的关键条目）
    city_mappings = {
        '大连交通大学': '大连', '大连理工大学': '大连', '大连海事大学': '大连', '大连大学': '大连',
        '合肥南站': '合肥', '合肥站': '合肥', '合肥西站': '合肥', '合肥东站': '合肥',
        '北京南站': '北京', '北京西站': '北京', '北京站': '北京', '北京交通大学': '北京',
        '上海虹桥站': '上海', '上海南站': '上海', '上海站': '上海',
        '广州南站': '广州', '广州站': '广州', '深圳北站': '深圳', '深圳站': '深圳',
        '南京南站': '南京', '南京站': '南京', '武汉站': '武汉', '成都东站': '成都', '成都站': '成都',
        '重庆北站': '重庆', '重庆站': '重庆', '西安北站': '西安', '西安站': '西安',
        '郑州东站': '郑州', '郑州站': '郑州', '杭州东站': '杭州', '杭州站': '杭州',
    }
    # 1) 完全匹配
    if n in city_mappings:
        return city_mappings[n]
    # 2) 子串匹配（例如"北京交通大学海淀校区"）
    try:
        for key, val in city_mappings.items():
            if key in n:
                return val
    except Exception:
        pass
    for suf in ['市', '站', '南站', '北站', '东站', '西站', '火车站', '高铁站', '大学', '学院', '学校']:
        n = n.replace(suf, '')
    return n

def _gaode_get_cities(timeout_seconds: int = 8, max_retries: int = 2) -> List[str]:
    """通过高德"行政区划查询"获取中国城市列表（level=city）。"""
    api_key = GAODE_API_KEY
    url = "https://restapi.amap.com/v3/config/district"
    params = {
        'key': api_key,
        'keywords': '中国',
        'subdistrict': 2,   # 省、市
        'extensions': 'base'
    }
    last_exc = None
    for attempt in range(max_retries + 1):
        try:
            resp = requests.get(url, params=params, timeout=timeout_seconds)
            if resp.status_code != 200:
                last_exc = Exception(f"HTTP {resp.status_code}")
            else:
                data = resp.json() if hasattr(resp, 'json') else {}
                if str(data.get('status')) != '1':
                    last_exc = Exception(f"gaode status={data.get('status')} info={data.get('info')}")
                else:
                    # 结构：districts -> [中国] -> districts(省) -> districts(市)
                    result: List[str] = []
                    try:
                        china = (data.get('districts') or [])[0] or {}
                        provinces = china.get('districts') or []
                        for prov in provinces:
                            for city in (prov.get('districts') or []):
                                name = (city.get('name') or '').strip()
                                if name and name not in result:
                                    result.append(name)
                    except Exception:
                        result = []
                    return result
        except Exception as e:
            last_exc = e
        time.sleep(0.5 * (attempt + 1))
    print(f"❌ 高德城市列表获取失败: {last_exc}")
    return []

def _gaode_geocode(address: str, timeout_seconds: int = 8, max_retries: int = 2) -> Dict[str, Any]:
    """高德地理编码：返回 {lng, lat, province, city, district, level}，失败返回 {}"""
    api_key = GAODE_API_KEY
    url = "https://restapi.amap.com/v3/geocode/geo"
    params = { 'key': api_key, 'address': address }
    last_exc = None
    for attempt in range(max_retries + 1):
        try:
            r = requests.get(url, params=params, timeout=timeout_seconds)
            if r.status_code != 200:
                last_exc = Exception(f"HTTP {r.status_code}")
            else:
                data = r.json()
                if str(data.get('status')) != '1':
                    last_exc = Exception(data.get('info'))
                else:
                    geos = data.get('geocodes') or []
                    if geos:
                        g = geos[0]
                        loc = (g.get('location') or '').split(',')
                        lng, lat = (float(loc[0]), float(loc[1])) if len(loc) == 2 else (None, None)
                        return {
                            'lng': lng, 'lat': lat,
                            'province': g.get('province',''),
                            'city': g.get('city','') if g.get('city') else g.get('district',''),
                            'district': g.get('district',''),
                            'level': g.get('level','')
                        }
                    return {}
        except Exception as e:
            last_exc = e
        time.sleep(0.4 * (attempt + 1))
    print(f"❌ 高德地理编码失败: {address} => {last_exc}")
    return {}

def _resolve_province(name_or_address: str) -> str:
    info = _gaode_geocode(name_or_address) if name_or_address else {}
    province = (info.get('province') or '').strip()
    
    # 兜底映射：如果地理编码失败，使用城市到省份的映射
    if not province:
        city_province_map = {
            '合肥': '安徽', '南京': '江苏', '杭州': '浙江',
            '北京': '北京', '上海': '上海', '天津': '天津',
            '重庆': '重庆', '大连': '辽宁', '沈阳': '辽宁',
            '武汉': '湖北', '长沙': '湖南', '广州': '广东',
            '深圳': '广东', '成都': '四川', '西安': '陕西'
        }
        province = city_province_map.get(_norm_city(name_or_address), '')
    
    # 标准化省份名称：去掉"省"、"市"等后缀
    if province:
        province = province.replace('省', '').replace('市', '').replace('自治区', '').replace('特别行政区', '')
    
    return province

def _is_reasonable_transfer_distance(origin_city: str, destination_city: str) -> bool:
    """判断中转距离是否合理，避免推荐不现实的远距离中转"""
    # 定义不合理的远距离中转组合
    unreasonable_pairs = {
        ('沈阳', '合肥'), ('沈阳', '南京'), ('沈阳', '杭州'),
        ('哈尔滨', '合肥'), ('哈尔滨', '南京'), ('哈尔滨', '杭州'),
        ('长春', '合肥'), ('长春', '南京'), ('长春', '杭州'),
        ('乌鲁木齐', '合肥'), ('乌鲁木齐', '南京'), ('乌鲁木齐', '杭州'),
        ('拉萨', '合肥'), ('拉萨', '南京'), ('拉萨', '杭州'),
    }
    
    # 检查是否是不合理的组合
    pair = (_norm_city(origin_city), _norm_city(destination_city))
    reverse_pair = (pair[1], pair[0])
    
    if pair in unreasonable_pairs or reverse_pair in unreasonable_pairs:
        return False
    
    return True

def _remove_duplicate_routes(routes: List[Dict]) -> List[Dict]:
    """移除重复的路线方案"""
    seen = set()
    unique_routes = []
    
    for route in routes:
        # 创建路线的唯一标识
        route_key = _create_route_key(route)
        if route_key not in seen:
            seen.add(route_key)
            unique_routes.append(route)
        else:
            print(f"   移除重复方案: {route.get('type', '')} - {route.get('origin', '')} -> {route.get('destination', '')}")
    
    print(f"   去重后方案数量: {len(unique_routes)} (原{len(routes)}条)")
    return unique_routes

def _create_route_key(route: Dict) -> str:
    """创建路线的唯一标识，基于时间、价格、路线去重"""
    if route.get('type') == 'flight':
        # 直飞：基于时间、价格、路线（不包含航班号，因为相同时间价格的航班应该去重）
        price = route.get('price') or route.get('total_price') or 0
        return f"flight:{route.get('origin', '')}:{route.get('destination', '')}:{route.get('dep_time', '')}:{route.get('arr_time', '')}:{price}"
    elif route.get('type') == 'multimodal':
        # 联运：基于主要交通工具信息（更严格：包含航班号/车次/中转城市）
        details = route.get('details', [])
        flight_segments = [d for d in details if d.get('type') == 'flight']
        train_segments = [d for d in details if d.get('type') == 'high_speed_rail']
        transfer_city = route.get('transfer_city','')
        
        key_parts = []
        if flight_segments:
            f = flight_segments[0]
            key_parts.append(
                f"flight:{_norm_city(f.get('origin',''))}:{_norm_city(f.get('destination',''))}:{f.get('departure_time','')}:{f.get('arrival_time','')}:{f.get('flight_number','')}"
            )
        if train_segments:
            t = train_segments[0]
            key_parts.append(
                f"train:{_norm_city(t.get('origin',''))}:{_norm_city(t.get('destination',''))}:{t.get('departure_time','')}:{t.get('arrival_time','')}:{t.get('train_number','')}:{t.get('to_station_name','')}"
            )
        if transfer_city:
            key_parts.append(f"xfer:{_norm_city(transfer_city)}")
        
        return ":".join(key_parts) if key_parts else f"multimodal:{route.get('origin', '')}:{route.get('destination', '')}"
    else:
        # 其他类型：基于基本信息
        price = route.get('price') or route.get('total_price') or 0
        return f"{route.get('type', '')}:{route.get('origin', '')}:{route.get('destination', '')}:{route.get('dep_time', '')}:{route.get('arr_time', '')}:{price}"

def get_flight_data_hub_first(origin_city: str, destination_city: str, date: str) -> List[Dict]:
    """航班优先策略：直达IATA → 目的地省会枢纽 → （可选）双方省会枢纽。
    仅使用真实API，按次序尝试，第一组有结果即返回。
    """
    def city_to_iata(city: str) -> str:
        return CITY_TO_IATA.get(_norm_city(city), _norm_city(city))

    flights_union: List[Dict] = []
    print(f"🔍 航班优先策略: {origin_city} → {destination_city} ({date})")

    # 1) 直达城市级 IATA（保留，用于同城接驳/展示）
    direct = get_flight_data_strict(_norm_city(origin_city), _norm_city(destination_city), date)
    flights_union.extend(direct or [])

    # 2) 目的地省会枢纽
    dest_prov = _resolve_province(destination_city)
    print(f"   目的地省份: {dest_prov}")
    hub = PROVINCE_CAPITAL_HUB.get(dest_prov)
    if hub:
        hub_city = hub.get('city')
        print(f"   省会枢纽: {hub_city}")
        via_dest_hub = get_flight_data_strict(_norm_city(origin_city), _norm_city(hub_city), date)
        if via_dest_hub:
            print(f"   省会枢纽航班: {len(via_dest_hub)} 条")
            for f in via_dest_hub:
                f['is_nearby'] = True
                f['nearby_type'] = 'destination'
                f['nearby_city'] = hub_city
                f['destination'] = f"{hub_city}({_norm_city(destination_city)}附近)"
            flights_union.extend(via_dest_hub)
        else:
            print(f"   省会枢纽航班: 无")
    else:
        print(f"   省会枢纽: 未找到")

    # 2.1) 目的地常见跨省优选枢纽（如 安徽→南京 ）
    extra_hubs = PROVINCE_ADJACENT_HUBS.get(dest_prov, [])
    print(f"   跨省优选枢纽: {extra_hubs}")
    for eh in extra_hubs:
        print(f"   尝试枢纽: {eh}")
        via_extra = get_flight_data_strict(_norm_city(origin_city), _norm_city(eh), date)
        if via_extra:
            print(f"   枢纽航班: {len(via_extra)} 条")
            for f in via_extra:
                f['is_nearby'] = True
                f['nearby_type'] = 'destination'
                f['nearby_city'] = eh
                f['destination'] = f"{eh}({_norm_city(destination_city)}附近)"
            flights_union.extend(via_extra)
        else:
            print(f"   枢纽航班: 无")

    # 3) 双方省会（出发省会 → 目的地省会），作为最后回退
    # 但需要检查距离是否合理（避免沈阳到合肥这种不现实的方案）
    orig_prov = _resolve_province(origin_city)
    hub_o = PROVINCE_CAPITAL_HUB.get(orig_prov)
    hub_d = PROVINCE_CAPITAL_HUB.get(dest_prov)
    if hub_o and hub_d:
        # 检查距离是否合理（例如沈阳到合肥太远，不应该推荐）
        reasonable_distance = _is_reasonable_transfer_distance(hub_o['city'], hub_d['city'])
        if reasonable_distance:
            via_both = get_flight_data_strict(_norm_city(hub_o['city']), _norm_city(hub_d['city']), date)
            if via_both:
                for f in via_both:
                    f['is_nearby'] = True
                    f['nearby_type'] = 'origin'
                    f['nearby_city'] = hub_o['city']
                    f['origin'] = f"{hub_o['city']}({_norm_city(origin_city)}附近)"
                    f['destination'] = f"{hub_d['city']}({_norm_city(destination_city)}附近)"
                flights_union.extend(via_both)
                print(f"   双方省会航班: {len(via_both)} 条")
            else:
                print(f"   双方省会航班: 无")
        else:
            print(f"   双方省会距离过远，跳过: {hub_o['city']} -> {hub_d['city']}")

    return flights_union

def http_get_with_retry(url, params, timeout_seconds=25, max_retries=2):
    session = requests.Session()
    last_exc = None
    for attempt in range(max_retries + 1):
        try:
            resp = session.get(url, params=params, timeout=timeout_seconds)
            return resp
        except Exception as e:
            last_exc = e
            backoff = 1.5 ** attempt
            print(f"⚠️ 航班API请求失败，第{attempt+1}次：{e}，{('重试中' if attempt < max_retries else '不再重试')}…")
            if attempt < max_retries:
                time.sleep(backoff)
    raise last_exc

# 航班（严格无模拟）

def get_flight_data_strict(origin: str, destination: str, date: str) -> List[Dict]:
    try:
        dep_code = CITY_TO_IATA.get(_norm_city(origin), origin)
        arr_code = CITY_TO_IATA.get(_norm_city(destination), destination)
        url = "https://apis.juhe.cn/flight/query"
        params = {
            'key': JUHE_FLIGHT_APPKEY,
            'dtype': 'json',
            'departure': dep_code,
            'arrival': arr_code,
            'departureDate': date
        }
        print(f"✈️ 获取机票数据: {origin}({dep_code}) → {destination}({arr_code})")
        response = http_get_with_retry(url, params=params, timeout_seconds=25, max_retries=2)
        if response.status_code != 200:
            print(f"❌ 机票API请求失败: {response.status_code}")
            return []
        result = response.json()
        if result.get('error_code') != 0:
            print(f"❌ 机票API错误: {result.get('reason', '未知')}")
            return []
        flight_list = result.get('result', {}).get('flightInfo', [])
        flights = []
        for item in flight_list:
            try:
                dep_time = item.get('departureTime', '')
                arr_time = item.get('arrivalTime', '')
                dep_date = item.get('departureDate', '')
                arr_date = item.get('arrivalDate', '')
                total_time = 0.0
                try:
                    dep_dt_str = f"{dep_date} {dep_time}".strip()
                    arr_dt_str = f"{arr_date} {arr_time}".strip()
                    from datetime import datetime as _dt
                    dep_dt = _dt.strptime(dep_dt_str, '%Y-%m-%d %H:%M')
                    arr_dt = _dt.strptime(arr_dt_str, '%Y-%m-%d %H:%M')
                    if arr_dt < dep_dt:
                        arr_dt = arr_dt + timedelta(days=1)
                    total_minutes = int((arr_dt - dep_dt).total_seconds() // 60)
                    total_time = max(0.5, round(total_minutes / 60.0, 1))
                except Exception:
                    pass
                price_raw = item.get('ticketPrice', 0)
                try:
                    price_val = int(price_raw or 0)
                except Exception:
                    price_val = 0
                price_unknown = (price_val <= 0)
                # 简单判定疑似中转或超长等待：>8小时 或 到达日期与出发日期不同
                suspected_transfer = False
                try:
                    suspected_transfer = (total_time and total_time >= 8.0) or (str(arr_date).strip() != str(dep_date).strip())
                except Exception:
                    suspected_transfer = False
                flights.append({
                    'type': 'flight',
                    'flight_number': item.get('flightNo', ''),
                    'airline': item.get('airlineName', ''),
                    'origin': origin,
                    'destination': destination,
                    'dep_time': f"{item.get('departureDate', '')} {dep_time}",
                    'arr_time': f"{item.get('arrivalDate', '')} {arr_time}",
                    'price': price_val if price_val>0 else 0,
                    'dep_airport': item.get('departureName', ''),
                    'arr_airport': item.get('arrivalName', ''),
                    'duration': f"{dep_time} - {arr_time}",
                    'source': '聚合数据API',
                    'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    'total_time': total_time,
                    'total_price': (price_val if price_val>0 else None),
                    'transfers': 1 if suspected_transfer else 0,
                    'badge': '中转' if suspected_transfer else '',
                    'suspected_transfer': suspected_transfer,
                    'details': [{
                        'type': 'flight', 'origin': origin, 'destination': destination,
                        'departure_time': dep_time, 'arrival_time': arr_time, 'price': (price_val if price_val>0 else 0),
                        'airline': item.get('airlineName', ''), 'flight_number': item.get('flightNo', '')
                    }]
                })
            except Exception as e:
                print(f"  ❌ 解析航班条目异常: {e}")
        print(f"✅ 获取到 {len(flights)} 条机票数据（严格模式）")
        return flights
    except Exception as e:
        print(f"❌ 机票API异常: {e}")
        return []

# 12306 工具（严格无模拟）

def http_get_json(session: requests.Session, url: str, params: Dict[str, Any], tries=2, timeout=10):
    last = None
    for i in range(tries):
        try:
            r = session.get(url, params=params, headers=DEFAULT_HEADERS, timeout=timeout, verify=False)
            if r.status_code == 200:
                return r.json()
            last = f"HTTP {r.status_code}"
        except Exception as e:
            last = str(e)
        time.sleep(0.4 * (i + 1))
    raise RuntimeError(f"GET {url} failed: {last}")

def load_station_map(session: requests.Session) -> Dict[str, str]:
    urls = [
        "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js",
    ]
    text = ""
    for u in urls:
        try:
            resp = session.get(u, headers=DEFAULT_HEADERS, timeout=12, verify=False)
            if resp.status_code == 200 and "station" in resp.text:
                text = resp.text
                break
        except Exception:
            continue
    if not text:
        return {}
    raw = None
    for p in [
        r"station_name\s*=\s*'([^']+)'",
        r"var\s+station_name\s*=\s*'([^']+)'",
        r"station_names\s*=\s*'([^']+)'",
        r"var\s+station_names\s*=\s*'([^']+)'",
    ]:
        m = re.search(p, text)
        if m:
            raw = m.group(1)
            break
    mapping: Dict[str, str] = {}
    if raw:
        for seg in raw.split("@"):
            if not seg:
                continue
            parts = seg.split("|")
            if len(parts) >= 3:
                name_cn = parts[1].strip()
                code = parts[2].strip()
                if name_cn and code:
                    mapping[name_cn] = code
    return mapping

def query_price(session: requests.Session, date: str, train_no: str, from_no: str, to_no: str, seat_types: str) -> Dict[str, int]:
    params = {
        "train_no": train_no,
        "from_station_no": from_no,
        "to_station_no": to_no,
        "seat_types": seat_types or "",
        "train_date": date,
    }
    def parse_money(v: Any) -> int:
        if not v:
            return 0
        m = re.search(r"([\d\.]+)", str(v))
        return int(round(float(m.group(1)))) if m else 0
    try:
        data = http_get_json(session, "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice", params, tries=2, timeout=8).get("data") or {}
    except Exception:
        data = {}
    return {
        "商务座": parse_money(data.get("A9")),
        "一等座": parse_money(data.get("M")),
        "二等座": parse_money(data.get("O")),
        "硬座":   parse_money(data.get("A1")),
        "硬卧":   parse_money(data.get("A3")),
        "软卧":   parse_money(data.get("A4")),
    }

def enrich_with_price(session: requests.Session, date: str, trains: List[Dict], limit: int = 80) -> None:
    requests.packages.urllib3.disable_warnings()
    for idx, t in enumerate(trains[:max(0, limit)]):
        try:
            price_map = query_price(
                session=session,
                date=date,
                train_no=t.get("train_no", ""),
                from_no=t.get("from_station_no", ""),
                to_no=t.get("to_station_no", ""),
                seat_types=t.get("seat_types", ""),
            )
            old_pm = t.get("price_map") or {}
            def _has_nonzero(pm: Dict[str, int]) -> bool:
                try:
                    return any(int(v or 0) > 0 for v in pm.values())
                except Exception:
                    return False
            if _has_nonzero(price_map):
                t["price_map"] = price_map or {}
            elif not old_pm:
                t["price_map"] = price_map or {}
            else:
                t["price_map"] = old_pm
            pm_eff = t.get("price_map") or {}
            p = (pm_eff.get("二等座") or pm_eff.get("一等座") or pm_eff.get("商务座")
                 or pm_eff.get("硬座") or pm_eff.get("硬卧") or pm_eff.get("软卧") or 0)
            t["price_min"] = int(p or 0)
        except Exception:
            t["price_map"] = {}
            t["price_min"] = 0


def _is_available(v: str) -> bool:
    if not v: return False
    v = str(v).strip()
    if v == '无': return False
    if v == '有': return True
    try:
        num = int(re.findall(r'\d+', v)[0])
        return num > 0
    except Exception:
        return False


def _choose_best_seat(t: Dict) -> Dict:
    price_map = t.get('price_map', {}) or {}
    candidates = [
        ('二等座', t.get('ed_left')),
        ('一等座', t.get('yd_left')),
        ('商务座', t.get('swz_left')),
        ('硬座',   t.get('yz_left')),
        ('硬卧',   t.get('yw_left')),
        ('软卧',   t.get('rw_left')),
    ]
    best = {'seat_label': '', 'seat_price': 0, 'seat_left': '', 'sold_out': False}

    if not any(_is_available(left) for _, left in candidates):
        return {'seat_label': '无票', 'seat_price': 0, 'seat_left': '无', 'sold_out': True}

    for seat_label, left in candidates:
        if _is_available(left) and price_map.get(seat_label):
            best = {'seat_label': seat_label, 'seat_price': int(price_map[seat_label] or 0), 'seat_left': left or '', 'sold_out': False}
            best['price_source'] = 'exact'
            break

    if not best['seat_label']:
        for seat_label, left in candidates:
            if _is_available(left):
                best = {'seat_label': seat_label, 'seat_price': 0, 'seat_left': left or '', 'sold_out': False}
                best['price_source'] = 'unknown'
                break

    if not best['seat_label']:
        seat_types_code = (t.get('seat_types') or '').strip()
        code_to_label = {
            'O': '二等座', 'M': '一等座', 'A9': '商务座',
            'A1': '硬座', 'A3': '硬卧', 'A4': '软卧'
        }
        picked_label = ''
        for code, label in [('O','二等座'), ('M','一等座'), ('A9','商务座'), ('A1','硬座'), ('A3','硬卧'), ('A4','软卧')]:
            if code in seat_types_code:
                picked_label = code_to_label.get(code, label)
                break
        if not picked_label:
            picked_label = '二等座'
        best = {
            'seat_label': picked_label,
            'seat_price': int(t.get('price_min') or 0),
            'seat_left': t.get({
                '二等座': 'ed_left', '一等座': 'yd_left', '商务座': 'swz_left',
                '硬座': 'yz_left', '硬卧': 'yw_left', '软卧': 'rw_left',
            }.get(picked_label, ''), '') or '',
            'sold_out': False,
            'price_source': 'min_estimate'
        }

    if best['seat_label'] and best.get('price_source') != 'unknown' and best['seat_price'] == 0 and price_map.get(best['seat_label']):
        try:
            best['seat_price'] = int(price_map.get(best['seat_label']) or 0)
            best['price_source'] = 'exact'
        except Exception:
            pass

    if best.get('price_source') not in ('unknown',) and best['seat_price'] == 0:
        best['seat_price'] = int(t.get('price_min') or 0)
        if best['seat_price'] > 0 and not best.get('price_source'):
            best['price_source'] = 'min_estimate'
    return best

# 严格的 12306 查询

def _get_json_strict(sess, url, params):
    r = sess.get(url, params=params, headers=DEFAULT_HEADERS, timeout=8, verify=False)
    txt = r.text.strip() if hasattr(r, "text") else ""
    if r.status_code != 200 or not txt or txt[0] not in "{[":
        raise RuntimeError(f"non-json resp, code={r.status_code}, head={txt[:80]}")
    return r.json()

def crawl_12306_trains_realtime_strict(date: str, origin: str, destination: str) -> List[Dict]:
    origin = _norm_city(origin); destination = _norm_city(destination)
    cache_key = f"rail-strict:{origin}:{destination}:{date}"
    cached = RAIL_CACHE.get(cache_key)
    if cached is not None:
        return cached

    session = requests.Session()
    session.headers.update(DEFAULT_HEADERS)
    session.headers.update({"X-Requested-With": "XMLHttpRequest"})
    requests.packages.urllib3.disable_warnings()

    try:
        session.get("https://kyfw.12306.cn/otn/leftTicket/init", timeout=8, verify=False)
    except Exception:
        pass

    station_map = load_station_map(session)
    from_code = station_map.get(origin)
    to_code = station_map.get(destination)
    if not from_code or not to_code:
        print(f"❌ 站点映射失败: {origin}->{from_code}, {destination}->{to_code}")
        RAIL_CACHE[cache_key] = []
        return []

    params = {
        "leftTicketDTO.train_date": date,
        "leftTicketDTO.from_station": from_code,
        "leftTicketDTO.to_station": to_code,
        "purpose_codes": "ADULT",
    }

    trains = []
    for ep in QUERY_ENDPOINTS:
        try:
            obj = _get_json_strict(session, ep, params)
            data = (obj or {}).get("data") or {}
            result_list = data.get("result") or []
            map_info = data.get("map") or {}
            for raw in result_list:
                if not raw:
                    continue
                fields = raw.split("|")
                if len(fields) < 40:
                    continue
                trains.append({
                    "train_code": fields[3],
                    "dep_time": fields[8],
                    "arr_time": fields[9],
                    "duration": fields[10],
                    "from_station_name": map_info.get(fields[6], fields[6]),
                    "to_station_name": map_info.get(fields[7], fields[7]),
                    "seat_types": fields[35],
                    "ed_left": (fields[30] or ""),
                    "yd_left": (fields[31] or ""),
                    "swz_left": (fields[32] or ""),
                    "yz_left": (fields[29] or ""),
                    "yw_left": (fields[28] or ""),
                    "rw_left": (fields[24] or ""),
                    "train_no": fields[2],
                    "from_station_no": fields[16],
                    "to_station_no": fields[17],
                })
            break
        except Exception as e:
            print(f"端点失败 {ep}: {e}")
            time.sleep(0.4)
            continue

    # 不使用任何模拟数据
    if not trains:
        RAIL_CACHE[cache_key] = []
        return []

    enrich_with_price(session, date, trains, limit=min(len(trains), 80))
    RAIL_CACHE[cache_key] = trains or []
    return trains or []

# 路线转换

def _parse_hours(dep: str, arr: str) -> float:
    try:
        if not dep or not arr:
            return 0.0
        from datetime import datetime as _dt
        d1 = _dt.strptime(dep.strip(), '%H:%M')
        d2 = _dt.strptime(arr.strip(), '%H:%M')
        delta = (d2 - d1).total_seconds() / 3600.0
        if delta < 0:
            delta += 24
        return round(max(0.0, delta), 1)
    except Exception:
        return 0.0


def find_train_routes_strict(origin: str, destination: str, date: str) -> List[Dict]:
    trains = crawl_12306_trains_realtime_strict(date, origin, destination)
    routes = []
    for t in trains:
        best = _choose_best_seat(t)
        routes.append({
            "type": "high_speed_rail",
            "origin": origin,
            "destination": destination,
            "dep_time": f"{date} {t.get('dep_time','')}",
            "arr_time": f"{date} {t.get('arr_time','')}",
            "duration": t.get("duration",""),
            "price": 0 if best.get('sold_out') else best['seat_price'],
            "train_code": t.get("train_code",""),
            "train_number": t.get("train_code",""),
            "seat_selected": best['seat_label'],
            "seat_left": best['seat_left'],
            "sold_out": bool(best.get('sold_out')),
            "badge": "无票" if best.get('sold_out') else "",
            "price_source": best.get('price_source',''),
                            "details": [{
                    "type": "high_speed_rail",
                    "train_number": t.get("train_code",""),
                    "origin": t.get("from_station_name", origin),
                    "destination": t.get("to_station_name", destination),
                    "from_station_name": t.get("from_station_name", origin),
                    "to_station_name": t.get("to_station_name", destination),
                    "departure_time": t.get("dep_time",""),
                    "arrival_time": t.get("arr_time",""),
                    "duration": t.get("duration",""),
                    "price": 0 if best.get('sold_out') else best['seat_price'],
                    "seat_selected": best['seat_label'],
                    "seat_left": best['seat_left'],
                    "price_source": best.get('price_source',''),
                    "seat_info": {
                    "二等座": t.get("ed_left",""),
                    "一等座": t.get("yd_left",""),
                    "商务座": t.get("swz_left",""),
                    "硬座": t.get("yz_left",""),
                    "硬卧": t.get("yw_left",""),
                    "软卧": t.get("rw_left",""),
                },
                "price_map": t.get("price_map", {}),
                "train_no": t.get("train_no",""),
                "from_station_no": t.get("from_station_no",""),
                "to_station_no": t.get("to_station_no",""),
                "seat_types": t.get("seat_types",""),
            }],
            "transfer": "",
            "total_price": 0 if best.get('sold_out') else best['seat_price'],
            "total_duration": t.get("duration",""),
            "total_time": _parse_hours(t.get("dep_time",""), t.get("arr_time","")),
            "transfers": 0,
        })
    return routes


def find_train_routes_with_earliest(origin: str, destination: str, date: str, earliest_hhmm: str, window_minutes: int = 120, max_options: int = 3) -> List[Dict]:
    trains = crawl_12306_trains_realtime_strict(date, origin, destination)
    routes = []
    try:
        from datetime import datetime as _dt, timedelta as _td
        up_bound = ( _dt.strptime(earliest_hhmm, '%H:%M') + _td(minutes=max(0, int(window_minutes or 0))) ).strftime('%H:%M')
    except Exception:
        up_bound = None
    for t in trains:
        dep = t.get("dep_time","")
        if not dep:
            continue
        if dep >= earliest_hhmm and (dep <= up_bound if up_bound else True):
            best = _choose_best_seat(t)
            routes.append({
                "type": "high_speed_rail",
                "origin": origin,
                "destination": destination,
                "dep_time": f"{date} {dep}",
                "arr_time": f"{date} {t.get('arr_time','')}",
                "duration": t.get("duration",""),
                "price": 0 if best.get('sold_out') else best['seat_price'],
                "train_code": t.get("train_code",""),
                "train_number": t.get("train_code",""),
                "seat_selected": best['seat_label'],
                "seat_left": best['seat_left'],
                "sold_out": bool(best.get('sold_out')),
                "badge": "无票" if best.get('sold_out') else "",
                "price_source": best.get('price_source',''),
                "details": [{
                    "type": "high_speed_rail",
                    "train_number": t.get("train_code",""),
                    "origin": t.get("from_station_name", origin),
                    "destination": t.get("to_station_name", destination),
                    "from_station_name": t.get("from_station_name", origin),
                    "to_station_name": t.get("to_station_name", destination),
                    "departure_time": dep,
                    "arrival_time": t.get("arr_time",""),
                    "duration": t.get("duration",""),
                    "price": 0 if best.get('sold_out') else best['seat_price'],
                    "seat_selected": best['seat_label'],
                    "seat_left": best['seat_left'],
                    "price_source": best.get('price_source',''),
                    "seat_info": {
                        "二等座": t.get("ed_left",""),
                        "一等座": t.get("yd_left",""),
                        "商务座": t.get("swz_left",""),
                        "硬座": t.get("yz_left",""),
                        "硬卧": t.get("yw_left",""),
                        "软卧": t.get("rw_left",""),
                    },
                    "price_map": t.get("price_map", {}),
                    "train_no": t.get("train_no",""),
                    "from_station_no": t.get("from_station_no",""),
                    "to_station_no": t.get("to_station_no",""),
                    "seat_types": t.get("seat_types",""),
                }],
                "transfer": "",
                "total_price": 0 if best.get('sold_out') else best['seat_price'],
                "total_duration": t.get("duration",""),
                "total_time": _parse_hours(t.get("dep_time",""), t.get("arr_time","")),
                "transfers": 0,
            })
    routes.sort(key=lambda r: (r.get('sold_out', False), r.get('total_time', 1e9), r.get('price', 1e9)))
    return routes[:max(1, int(max_options or 1))]

# 衔接：动态缓冲

def get_transport_hub_access_strict(origin: str, hub_name: str, hub_type: str = 'airport') -> Dict:
    try:
        gaode_api = get_gaode_api()
        if not gaode_api:
            return {}
        result = gaode_api.get_airport_station_access(origin, hub_name, hub_type)
        if result and result.get('options'):
            return result
        return {}
    except Exception as e:
        print(f"获取交通枢纽接驳信息失败: {e}")
        return {}


def build_flight_rail_combos(final_destination: str, date: str, flights: List[Dict], origin_address: str = None, origin: str = None, destination_address: str = None, optimization_type: str = 'time', primary_mode: str = 'auto') -> List[Dict]:
    combos: List[Dict] = []
    fd = _norm_city(final_destination)
    print(f" 开始构建联运方案(严格): 目标城市 {final_destination}({fd})")

    origin_to_airport = {}
    origin_to_station = {}
    if origin_address and origin:
        try:
            city = _norm_city(origin)
            airport_name = (CITY_MAJOR_AIRPORTS.get(city) or [f"{city}机场"])[0]
            # 起点接驳按 primary_mode 决定优先展示哪种
            if primary_mode in ('auto','flight'):
                origin_to_airport = get_transport_hub_access_strict(origin_address, airport_name, 'airport')
                if origin_to_airport:
                    origin_to_airport['chosen_airport'] = airport_name
            if primary_mode == 'rail':
                station_name = (CITY_MAJOR_STATIONS.get(city) or [f"{city}南站", f"{city}站"])[0]
                origin_to_station = get_transport_hub_access_strict(origin_address, station_name, 'railway_station')
                if origin_to_station:
                    origin_to_station['chosen_station'] = station_name
        except Exception as e:
            print(f" 获取起点到机场接驳失败: {e}")

    for f in flights:
        try:
            # 过滤：仅处理 目的地附近航班 或 同城落地航班；忽略 起点附近航班
            if f.get('is_nearby') and str(f.get('nearby_type')) == 'origin':
                continue
            mid_city = f.get('nearby_city') or f.get('destination') or ''
            # 允许两类：
            # 1) 目的地附近城市航班（nearby destination 模式）
            # 2) 同城落地（航班到达城市==最终目的地城市），用于构建机场→终点地址/车站的地面接驳
            mode_samecity = (_norm_city(f.get('destination','')) == fd)
            mode_nearby = f.get('is_nearby') and f.get('nearby_type') == 'destination' and not mode_samecity
            print(f"   航班: {f.get('origin','')} → {f.get('destination','')}, is_nearby={f.get('is_nearby')}, nearby_type={f.get('nearby_type')}, mode_nearby={mode_nearby}, mode_samecity={mode_samecity}")
            if not (mode_nearby or mode_samecity):
                print(f"   跳过: 不符合联运条件")
                continue
            mid = _norm_city(mid_city)
            if not mid:
                continue

            # 解析航班到达时间
            flight_arr_time = f.get('arr_time', '')
            if not flight_arr_time:
                continue

            try:
                # 提取 "HH:MM"
                flight_time_str = flight_arr_time.split(' ')[-1] if ' ' in flight_arr_time else flight_arr_time
                from datetime import datetime as _dt, timedelta as _td
                flight_arr_dt = _dt.strptime(flight_time_str, '%H:%M')

                city = _norm_city(mid_city)
                airport_name = (CITY_MAJOR_AIRPORTS.get(city) or [f"{city}机场"])[0]
                candidates = (CITY_MAJOR_STATIONS.get(city) or [f"{city}南站", f"{city}火车站"])

                chosen_station = None
                chosen_mode = None
                ground_minutes = None
                for cand in candidates:
                    info = get_transport_hub_access_strict(airport_name, cand, 'airport')
                    opts = (info or {}).get('options', {})
                    driving = opts.get('driving') or {}
                    transit = opts.get('transit') or {}
                    pick = None
                    if optimization_type == 'price' and transit.get('duration'):
                        pick = ('transit', transit.get('duration'))
                    elif driving.get('duration'):
                        pick = ('driving', driving.get('duration'))
                    if pick:
                        if ground_minutes is None or pick[1] < ground_minutes:
                            ground_minutes = pick[1]
                            chosen_mode = pick[0]
                            chosen_station = cand
                if ground_minutes is None:
                    print("  ❌ 无法获取机场到车站接驳时间，使用默认缓冲继续评估")
                    # 使用保守默认：驾车40分钟 + 缓冲45分钟
                    chosen_mode = 'driving'
                    ground_minutes = 40

                airport_scale = AIRPORT_SCALE.get(airport_name, DEFAULT_AIRPORT_SCALE)
                base_buf = CONNECTION_BUFFER_RULES['flight_to_rail'].get(airport_scale, CONNECTION_BUFFER_RULES['flight_to_rail'][DEFAULT_AIRPORT_SCALE]).get(chosen_mode, 45)
                hour_key = flight_arr_dt.strftime('%H')
                mult = float(HOUR_CONGESTION_MULTIPLIER.get(hour_key, 1.0))
                add_minutes = int(ground_minutes) + int(round(base_buf * mult))
                earliest_train_dep_dt = flight_arr_dt + _td(minutes=add_minutes)
                earliest_train_dep_time = earliest_train_dep_dt.strftime('%H:%M')
                print(f"   搜索 {mid_city} → {final_destination} 的高铁… 最早出发 {earliest_train_dep_time}（接驳{ground_minutes}分+缓冲{int(round(base_buf*mult))}分）")
            except Exception as e:
                print(f"  ❌ 解析航班/衔接时间失败: {e}")
                continue

            trains = []
            if mode_nearby:
                # 目的地附近城市 → 终点：用高铁联运（价格优先放宽窗口到4小时）
                win = 240 if optimization_type == 'price' else 120
                trains = find_train_routes_with_earliest(mid_city, final_destination, date, earliest_train_dep_time, window_minutes=win, max_options=3)
            if not trains and mode_nearby:
                # 回退：选当天所有车次里最便宜/最短的一条，保证能给出对比
                try:
                    all_trains = find_train_routes_strict(mid_city, final_destination, date)
                    if all_trains:
                        def _key_tr(t):
                            return (t.get('total_price') if t.get('total_price') else (t.get('price') or 1e9), t.get('total_time') or 1e9)
                        all_trains.sort(key=_key_tr)
                        trains = [all_trains[0]]
                        print(f"     ✅ 回退使用最便宜高铁: {trains[0].get('train_code','')}")
                    else:
                        # 进一步回退：尝试到目的地邻近城市（如池州→安庆等）
                        fb_list = DEST_CITY_FALLBACKS.get(_norm_city(final_destination), [])
                        for fb_city in fb_list:
                            print(f"     尝试邻近目的地回退: {mid_city} -> {fb_city}")
                            cand_trains = find_train_routes_with_earliest(mid_city, fb_city, date, earliest_train_dep_time, window_minutes=win, max_options=3)
                            if not cand_trains:
                                cand_all = find_train_routes_strict(mid_city, fb_city, date)
                                if cand_all:
                                    def _key2(t):
                                        return (t.get('total_price') if t.get('total_price') else (t.get('price') or 1e9), t.get('total_time') or 1e9)
                                    cand_all.sort(key=_key2)
                                    cand_trains = [cand_all[0]]
                            if cand_trains:
                                trains = cand_trains
                                # 将最终目的地调整为邻近城市，并在下方补地面接驳段
                                print(f"     ✅ 使用邻近目的地回退: {fb_city}")
                                # 为后续地面接驳记录
                                route_uses_fallback_city = fb_city
                                break
                except Exception as e:
                    print(f"     ❌ 回退获取高铁失败: {e}")
            
            # 同城落地模式：不需要高铁，直接处理地面接驳
            if mode_samecity and not mode_nearby:
                print(f"    同城落地模式：跳过高铁搜索，直接处理地面接驳")
                trains = []  # 同城落地不需要高铁
            elif not trains and not mode_samecity:
                print("     ❌ 窗口内无高铁，放弃该联运")
                continue

            t = (sorted(trains, key=lambda x: (x.get('total_time', 1e9), x.get('price', 1e9)))[0]) if trains else {}
            train_detail = t['details'][0] if (t and t.get('details')) else {}
            print(f"    高铁信息: t={bool(t)}, train_detail={bool(train_detail)}, mode_nearby={mode_nearby}")

            def _num(v):
                try:
                    return int(v)
                except Exception:
                    try:
                        return int(float(v))
                    except Exception:
                        return 0
            flight_price = _num(f.get('total_price') or f.get('price') or (f.get('details') and f['details'][0].get('price')))
            train_price = _num((t.get('total_price') if t else 0) or (t.get('price') if t else 0) or train_detail.get('price'))
            total_price = (flight_price + train_price) if (flight_price>0 and train_price>0 and not (t.get('sold_out', False) if t else False)) else None
            combo_price_source = 'exact' if total_price is not None else 'unknown'

            flight_time = f.get('total_time') or _parse_hours(str(f.get('dep_time',''))[-5:], str(f.get('arr_time',''))[-5:])
            train_time = (t.get('total_time') if t else 0) or _parse_hours(train_detail.get('departure_time',''), train_detail.get('arrival_time',''))

            total_time = None
            try:
                gt_minutes = 0
                # 起点地面接驳：按 primary_mode 选择展示
                if (primary_mode in ('auto','flight')) and origin_to_airport and origin_to_airport.get('options'):
                    if 'driving' in origin_to_airport['options']:
                        gt_minutes += origin_to_airport['options']['driving'].get('duration', 0)
                    elif 'transit' in origin_to_airport['options']:
                        gt_minutes += origin_to_airport['options']['transit'].get('duration', 0)
                if primary_mode == 'rail' and origin_to_station and origin_to_station.get('options'):
                    if 'driving' in origin_to_station['options']:
                        gt_minutes += origin_to_station['options']['driving'].get('duration', 0)
                    elif 'transit' in origin_to_station['options']:
                        gt_minutes += origin_to_station['options']['transit'].get('duration', 0)
                if ground_minutes:
                    gt_minutes += int(ground_minutes)
                total_time = round((flight_time or 0) + (train_time or 0) + (gt_minutes/60.0), 1)
            except Exception:
                total_time = round((flight_time or 0) + (train_time or 0), 1)

            details = []
            added_final_ground = False
            if (primary_mode in ('auto','flight')) and origin_to_airport and origin_to_airport.get('options'):
                to_airport = origin_to_airport['options'].get('driving') or origin_to_airport['options'].get('transit') or {}
                if to_airport:
                    details.append({
                        'type': 'ground_transport',
                        'origin': origin_address or f.get('origin'),
                        'destination': f"{f.get('origin')}机场",
                        'departure_time': '出发前',
                        'arrival_time': f.get('dep_time', ''),
                        'price': 0,
                        'transport_type': to_airport.get('type', 'driving'),
                        'duration': to_airport.get('duration', 0),
                        'distance': to_airport.get('distance', 0),
                        'description': to_airport.get('description', '')
                    })
            if primary_mode == 'rail' and origin_to_station and origin_to_station.get('options'):
                to_station = origin_to_station['options'].get('driving') or origin_to_station['options'].get('transit') or {}
                if to_station:
                    details.append({
                        'type': 'ground_transport',
                        'origin': origin_address or f.get('origin'),
                        'destination': (origin_to_station.get('chosen_station') or (CITY_MAJOR_STATIONS.get(_norm_city(origin)) or [f"{_norm_city(origin)}站"])[0]),
                        'departure_time': '出发前',
                        'arrival_time': f.get('dep_time', ''),
                        'price': 0,
                        'transport_type': to_station.get('type', 'driving'),
                        'duration': to_station.get('duration', 0),
                        'distance': to_station.get('distance', 0),
                        'description': to_station.get('description', '')
                    })

            details.append({
                'type': 'flight',
                'origin': f.get('origin'),
                'destination': mid_city,
                'departure_time': str(f.get('dep_time',''))[-5:],
                'arrival_time': str(f.get('arr_time',''))[-5:],
                'price': int(f.get('total_price') or f.get('price') or 0),
                'airline': f.get('airline'),
                'flight_number': f.get('flight_number')
            })
            # 添加高铁段（仅在附近城市模式且有高铁时）
            if mode_nearby and t and train_detail:
                print(f"    添加高铁段: {mid_city} -> {final_destination}")
                
                # 添加机场到高铁站的接驳（南京机场 -> 南京南站）
                airport_name = (CITY_MAJOR_AIRPORTS.get(_norm_city(mid_city)) or [f"{_norm_city(mid_city)}机场"])[0]
                station_name = (CITY_MAJOR_STATIONS.get(_norm_city(mid_city)) or [f"{_norm_city(mid_city)}南站"])[0]
                
                # 获取机场到高铁站的地面接驳信息
                airport_to_station = get_transport_hub_access_strict(airport_name, station_name, 'airport')
                if airport_to_station and airport_to_station.get('options'):
                    ground_option = airport_to_station['options'].get('driving') or airport_to_station['options'].get('transit') or {}
                    if ground_option:
                        print(f"    添加机场接驳: {airport_name} -> {station_name}")
                        details.append({
                            'type': 'ground_transport',
                            'origin': airport_name,
                            'destination': station_name,
                            'departure_time': f.get('arr_time', ''),
                            'arrival_time': '到达后',
                            'price': 0,
                            'transport_type': ground_option.get('type', 'driving'),
                            'duration': ground_option.get('duration', 0),
                            'distance': ground_option.get('distance', 0),
                            'description': ground_option.get('description', '')
                        })
                
                dest_city_for_train = locals().get('route_uses_fallback_city') or final_destination
                details.append({
                    'type': 'high_speed_rail',
                    'origin': mid_city,
                    'destination': dest_city_for_train,
                    'departure_time': train_detail.get('departure_time',''),
                    'arrival_time': train_detail.get('arrival_time',''),
                    'price': int(t.get('total_price') or t.get('price') or 0),
                    'train_number': train_detail.get('train_number',''),
                    'seat_selected': train_detail.get('seat_selected',''),
                    'seat_left': train_detail.get('seat_left',''),
                    'price_source': train_detail.get('price_source',''),
                    'sold_out': bool(t.get('sold_out', False)),
                    'train_no': t.get('details',[{}])[0].get('train_no',''),
                    'from_station_no': t.get('details',[{}])[0].get('from_station_no',''),
                    'to_station_no': t.get('details',[{}])[0].get('to_station_no',''),
                    'seat_types': t.get('details',[{}])[0].get('seat_types',''),
                    'to_station_name': train_detail.get('to_station_name','') or dest_city_for_train
                })
                
                # 添加高铁站到最终目的地的接驳（南京南站 -> 合肥南站）
                if destination_address and not added_final_ground:
                    to_station = train_detail.get('to_station_name') or (CITY_MAJOR_STATIONS.get(_norm_city(final_destination)) or [f"{_norm_city(final_destination)}站"])[0]
                    ground_transport_info = get_transport_hub_access_strict(to_station, destination_address, 'station')
                    if ground_transport_info and ground_transport_info.get('options'):
                        ground_option = ground_transport_info['options'].get('driving') or ground_transport_info['options'].get('transit') or {}
                        if ground_option:
                            print(f"    添加终点接驳: {to_station} -> {destination_address}")
                            details.append({
                                'type': 'ground_transport',
                                'origin': to_station,
                                'destination': destination_address,
                                'departure_time': train_detail.get('arrival_time',''),
                                'arrival_time': '到达后',
                                'price': 0,
                                'transport_type': ground_option.get('type', 'driving'),
                                'duration': ground_option.get('duration', 0),
                                'distance': ground_option.get('distance', 0),
                                'description': ground_option.get('description', '')
                            })
                            added_final_ground = True
            elif mode_samecity and destination_address:
                # 同城落地模式：添加机场到目的地的地面接驳
                print(f"    添加同城接驳: 机场 -> {destination_address}")
                # 获取机场到目的地的地面接驳信息
                airport_name = (CITY_MAJOR_AIRPORTS.get(_norm_city(mid_city)) or [f"{_norm_city(mid_city)}机场"])[0]
                ground_info = get_transport_hub_access_strict(airport_name, destination_address, 'airport')
                if ground_info and ground_info.get('options'):
                    ground_option = ground_info['options'].get('driving') or ground_info['options'].get('transit') or {}
                    if ground_option:
                        details.append({
                            'type': 'ground_transport',
                            'origin': airport_name,
                            'destination': destination_address,
                            'departure_time': f.get('arr_time', ''),
                            'arrival_time': '到达后',
                            'price': 0,
                            'transport_type': ground_option.get('type', 'driving'),
                            'duration': ground_option.get('duration', 0),
                            'distance': ground_option.get('distance', 0),
                            'description': ground_option.get('description', '')
                        })
                else:
                    # 兜底：用通用驾车/公交获取估算，避免因枢纽接口失败而缺失地面段
                    try:
                        gaode_api = get_gaode_api()
                        est = gaode_api.get_route_info(airport_name, destination_address, 'driving') or {}
                        if not est or est.get('duration') is None:
                            est = gaode_api.get_route_info(airport_name, destination_address, 'transit') or {}
                        if est and est.get('duration') is not None:
                            details.append({
                                'type': 'ground_transport',
                                'origin': airport_name,
                                'destination': destination_address,
                                'departure_time': f.get('arr_time', ''),
                                'arrival_time': '到达后',
                                'price': int(est.get('cost') or est.get('tolls') or 0),
                                'transport_type': 'driving' if est.get('tolls') is not None else 'transit',
                                'duration': int(est.get('duration') or 0),
                                'distance': int(est.get('distance') or 0),
                                'description': est.get('description', '') or '通用高德估算'
                            })
                        else:
                            print("    同城接驳兜底失败：无估算结果")
                    except Exception as _e:
                        print(f"    同城接驳兜底异常: {_e}")
            elif mode_samecity and not destination_address:
                # 未提供详细地址时的兜底：机场 → 城市中心
                try:
                    airport_name = (CITY_MAJOR_AIRPORTS.get(_norm_city(mid_city)) or [f"{_norm_city(mid_city)}机场"])[0]
                    city_center = _norm_city(final_destination)
                    # 用高德驾车/公交估算
                    gaode_api = get_gaode_api()
                    est = gaode_api.get_route_info(airport_name, city_center, 'driving') or {}
                    if not est or est.get('duration') is None:
                        est = gaode_api.get_route_info(airport_name, city_center, 'transit') or {}
                    if est and est.get('duration') is not None:
                        details.append({
                            'type': 'ground_transport',
                            'origin': airport_name,
                            'destination': f"{city_center}市中心",
                            'departure_time': f.get('arr_time', ''),
                            'arrival_time': '到达后',
                            'price': int(est.get('cost') or est.get('tolls') or 0),
                            'transport_type': 'driving' if est.get('tolls') is not None else 'transit',
                            'duration': int(est.get('duration') or 0),
                            'distance': int(est.get('distance') or 0),
                            'description': est.get('description', '') or '城市中心兜底接驳'
                        })
                except Exception as _e:
                    print(f"    城市中心兜底接驳失败: {_e}")

            # 火车站到终点的地面交通（仅在提供了详细地址时展示，且使用到达城市的车站名）
            ground_transport_info = {}
            if (not added_final_ground) and destination_address and train_detail.get('to_station_name'):
                to_station2 = train_detail['to_station_name']
                ground_transport_info = get_transport_hub_access_strict(to_station2, destination_address, 'station')
                if ground_transport_info and ground_transport_info.get('options'):
                    details.append({
                        'type': 'ground_transport',
                        'origin': to_station2,
                        'destination': destination_address,
                        'departure_time': train_detail.get('arrival_time',''),
                        'arrival_time': '到达后',
                        'price': 0,
                        'transport_type': ground_transport_info['options'].get('driving', {}).get('type', 'driving'),
                        'duration': ground_transport_info['options'].get('driving', {}).get('duration', 0),
                        'distance': ground_transport_info['options'].get('driving', {}).get('distance', 0),
                        'description': ground_transport_info['options'].get('driving', {}).get('description', '')
                    })
                    added_final_ground = True

            combos.append({
                'type': 'multimodal',
                'origin': f.get('origin'),
                'destination': final_destination,
                'dep_time': f.get('dep_time'),
                'arr_time': (t.get('arr_time') if t else f.get('arr_time')),
                'total_price': total_price,
                'price_source': combo_price_source,
                'total_time': total_time,
                'transfers': (1 if any(d.get('type') == 'high_speed_rail' for d in details) else 0),
                'transfer_city': mid_city,
                'transfer_time': f"{flight_time_str} → {train_detail.get('departure_time','') if t else '地面接驳'}",
                'details': details,
                'approx_price': (total_price is None)
            })

        except Exception as e:
            print(f"  ❌ 构建联运失败: {e}")
            continue
    print(f"✅ 构建联运方案完成(严格)")
    return combos

# 排序与分组（复用 liudang 风格，简化版）

def calculate_arrival_type(route: Dict) -> str:
    try:
        dep_time = route.get('dep_time', '')
        arr_time = route.get('arr_time', '')
        if not dep_time or not arr_time:
            return 'unknown'
        dep_time_str = dep_time.split(' ')[-1] if ' ' in dep_time else dep_time
        arr_time_str = arr_time.split(' ')[-1] if ' ' in arr_time else arr_time
        from datetime import datetime as _dt
        dep_dt = _dt.strptime(dep_time_str, '%H:%M')
        arr_dt = _dt.strptime(arr_time_str, '%H:%M')
        return 'next_day' if arr_dt < dep_dt else 'same_day'
    except Exception:
        return 'unknown'


def enhanced_sort_routes(routes: List[Dict], optimization_type: str, search_origin: str, search_destination: str) -> List[Dict]:
    def is_direct_route(r: Dict) -> bool:
        if r.get('transfers', 0) != 0:
            # 仍可能是仅航班+地面段但误标了换乘，放宽到详情判断
            pass
        first_o = r.get('origin') or (r.get('details',[{}])[0].get('origin',''))
        last_d = r.get('destination') or (r.get('details',[{}])[-1].get('destination',''))
        if _norm_city(first_o) != _norm_city(search_origin):
            return False
        if _norm_city(last_d) != _norm_city(search_destination):
            return False
        # 纯航班直达
        if r.get('type') in ('flight','high_speed_rail') and r.get('transfers',0) == 0:
            return True
        # multimodal 但仅包含 1 段航班 + 若干地面接驳（无高铁/二次航段），也视为"直达"
        if r.get('type') == 'multimodal':
            details = r.get('details', []) or []
            has_train = any(d.get('type') == 'high_speed_rail' for d in details)
            flight_count = sum(1 for d in details if d.get('type') == 'flight')
            if (not has_train) and flight_count == 1:
                return True
        return False

    for r in routes:
        r['arrival_type'] = calculate_arrival_type(r)
        # 综合优先时补充效用
        if optimization_type == 'composite' and 'utility' not in r:
            _compute_utility(r)

    direct_routes, same_day_combos, next_day_combos, others = [], [], [], []

    for r in routes:
        if is_direct_route(r):
            r['category'] = '直达'
            r['badge'] = '直达'
            direct_routes.append(r)
            continue
        is_combo = (r.get('type') == 'multimodal' or r.get('transfers', 0) == 1)
        if is_combo:
            if r.get('arrival_type') == 'same_day':
                r['category'] = '日内到达'
                r['badge'] = '日内到达'
                same_day_combos.append(r)
            elif r.get('arrival_type') == 'next_day':
                r['category'] = '隔日到达'
                r['badge'] = '隔日到达'
                next_day_combos.append(r)
            else:
                r['category'] = '其他'
                r['badge'] = '其他'
                others.append(r)
        else:
            r['category'] = '其他'
            r['badge'] = '其他'
            others.append(r)

    def sort_key(x: Dict):
        if optimization_type == 'price':
            return x.get('total_price') if x.get('total_price') is not None else x.get('price', 0)
        if optimization_type == 'composite':
            # 先按效用降序，平局按换乘数升序、再按总时长升序
            util = x.get('utility') if x.get('utility') is not None else _compute_utility(x)
            transfers = x.get('transfers', 0)
            timev = x.get('total_time', 1e9)
            return (-float(util or 0), int(transfers or 0), float(timev or 1e9))
        return x.get('total_time', 0)

    direct_routes.sort(key=sort_key)
    same_day_combos.sort(key=sort_key)
    next_day_combos.sort(key=sort_key)
    others.sort(key=sort_key)

    for r in direct_routes + same_day_combos + next_day_combos + others:
        if r.get('arrival_type') == 'same_day':
            r['arrival_label'] = '日内到达'
        elif r.get('arrival_type') == 'next_day':
            r['arrival_label'] = '隔日到达'
        else:
            r['arrival_label'] = '未知'

    return direct_routes + same_day_combos + next_day_combos + others

# 按航班段折叠联运方案：同一航班 + 不同高铁 → 只保留最优一条，其余交由前端下拉框选择

def _collapse_multimodal_by_flight(routes: List[Dict], optimization_type: str) -> List[Dict]:
    def _flight_key(r: Dict) -> str:
        try:
            if r.get('type') != 'multimodal':
                return ''
            flight = next((d for d in (r.get('details') or []) if d.get('type') == 'flight'), {})
            return "|".join([
                _norm_city(flight.get('origin','')),
                _norm_city(flight.get('destination','')),
                str(flight.get('flight_number','')),
                str(flight.get('departure_time','')),
                str(flight.get('arrival_time','')),
            ])
        except Exception:
            return ''

    def _score(r: Dict) -> tuple:
        if optimization_type == 'price':
            price = r.get('total_price') if r.get('total_price') is not None else r.get('price', 1e9)
            timev = r.get('total_time', 1e9)
            return (price, timev)
        timev = r.get('total_time', 1e9)
        price = r.get('total_price') if r.get('total_price') is not None else r.get('price', 1e9)
        return (timev, price)

    collapsed: Dict[str, Dict] = {}
    rest: List[Dict] = []

    for r in routes:
        if r.get('type') == 'multimodal':
            key = _flight_key(r)
            if not key:
                rest.append(r)
                continue
            prev = collapsed.get(key)
            if prev is None or _score(r) < _score(prev):
                collapsed[key] = r
        else:
            rest.append(r)

    return rest + list(collapsed.values())

# 直达航班去重：起降时间与价格完全相同的仅保留一条

def _dedup_direct_flights(routes: List[Dict]) -> List[Dict]:
    groups: Dict[str, Dict] = {}
    others: List[Dict] = []
    for r in routes:
        if r.get('type') == 'flight' and (r.get('transfers', 0) == 0):
            key = "|".join([
                _norm_city(r.get('origin','')),
                _norm_city(r.get('destination','')),
                str(r.get('dep_time',''))[-5:],
                str(r.get('arr_time',''))[-5:],
            ])
            price = r.get('total_price') if r.get('total_price') is not None else r.get('price', 0)
            prev = groups.get(key)
            if prev is None:
                groups[key] = r
            else:
                prev_price = prev.get('total_price') if prev.get('total_price') is not None else prev.get('price', 0)
                # 保留更低价格的那个
                if int(price or 0) < int(prev_price or 0):
                    groups[key] = r
        else:
            others.append(r)
    return others + list(groups.values())

# 基于logit的综合效用计算（价格-时间权衡 + 换乘/复杂性惩罚）

def _compute_utility(route: Dict) -> float:
    """计算路线的综合效用值，并在路由上标注 'utility' 字段。
    规则（单位已折算）：
      - 基础效用 = (-0.003 * 总价格元) + (-0.006 * 总时间分钟)
      - 换乘惩罚：每次 -0.36
      - 复杂性惩罚：当段数 > 4 时，额外 -0.24
    排序时按效用降序，平局时换乘次数更少优先。
    """
    try:
        # 总价格
        total_price = route.get('total_price')
        if total_price is None:
            # 汇总 details 中可用价格
            total_price = 0
            for d in route.get('details', []) or []:
                try:
                    p = d.get('price')
                    if p is not None:
                        total_price += int(p)
                except Exception:
                    continue
        try:
            total_price = int(total_price or 0)
        except Exception:
            total_price = 0

        # 总时间（分钟）
        total_time_hours = route.get('total_time') or 0
        try:
            total_time_minutes = float(total_time_hours or 0) * 60.0
        except Exception:
            total_time_minutes = 0.0

        # 换乘次数
        transfers = route.get('transfers')
        if transfers is None:
            transfers = max(0, len(route.get('details', []) or []) - 1)
        try:
            transfers = int(transfers or 0)
        except Exception:
            transfers = 0

        # 段数
        segments = len(route.get('details', []) or [])

        # 系数与惩罚
        price_coef = -0.003
        time_coef = -0.006
        transfer_penalty_per_transfer = -0.36
        complexity_penalty = -0.24 if segments > 4 else 0.0

        base_utility = price_coef * float(total_price) + time_coef * float(total_time_minutes)
        transfer_penalty = transfer_penalty_per_transfer * float(transfers)
        utility = float(base_utility + transfer_penalty + complexity_penalty)
        route['utility'] = utility
        route['utility_breakdown'] = {
            'base': base_utility,
            'transfer_penalty': transfer_penalty,
            'complexity_penalty': complexity_penalty,
        }
        return utility
    except Exception:
        route['utility'] = float('-inf')
        return float('-inf')

# HTTP 服务（严格版）

class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        static_root = os.path.dirname(__file__)
        super().__init__(*args, directory=static_root, **kwargs)

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()

    def do_GET(self):
        if self.path == '/':
            self.path = '/frontend/index.html'
        elif self.path.startswith('/api/'):
            self.handle_api_request()
            return
        super().do_GET()

    def do_POST(self):
        if self.path == '/api/search':
            self.handle_search_request()
        elif self.path == '/api/rail_options':
            self.handle_rail_options_request()
        elif self.path == '/api/rail_price':
            self.handle_rail_price_request()
        elif self.path == '/api/ground_options':
            self.handle_ground_options_request()
        elif self.path == '/api/poi':
            self.handle_poi_request()
        else:
            self.send_error(404)

    def handle_api_request(self):
        if self.path == '/api/status':
            self.send_api_status()
        elif self.path == '/api/cities':
            self.send_cities()
        elif self.path == '/api/options':
            self.handle_options_request()
        else:
            self.send_error(404)

    def handle_search_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        post_data = self.rfile.read(content_length)
        try:
            data = json.loads(post_data.decode('utf-8'))
        except:
            data = {}
        origin = data.get('origin', '北京')
        destination = data.get('destination', '上海')
        date = data.get('date') or data.get('departure_date')
        origin_address = data.get('origin_address', '')
        destination_address = data.get('destination_address', '')
        raw_opt = data.get('optimization_type')
        optimization_type = str(raw_opt).strip().lower() if raw_opt else 'time'
        # 兼容映射：将"舒适/综合/utility"等归一为综合优先
        if optimization_type in ('comfort','comfortable','综合','综合优先','utility','util','效用','logit'):
            optimization_type = 'composite'
        include_hidden = bool(data.get('include_hidden'))
        primary_mode = str(data.get('primary_mode') or 'auto').strip().lower()  # 'flight' | 'rail' | 'auto'

        # 统一安全日期（没传就用今天）
        from datetime import datetime as _dt
        safe_date = date or _dt.now().strftime('%Y-%m-%d')

        print(f"\n🔍 [严格] 搜索请求: {origin} → {destination} ({safe_date})")
        all_routes = []

        flights = get_flight_data_hub_first(_norm_city(origin), _norm_city(destination), safe_date)
        all_routes.extend(flights)
        print(f"   机票(严格): {len(flights)} 条")

        rail_routes = find_train_routes_strict(_norm_city(origin), _norm_city(destination), safe_date)
        all_routes.extend(rail_routes)
        print(f"   高铁(严格): {len(rail_routes)} 条")

        # 用真实机票结果本身构建联运（含省会枢纽回退的 is_nearby 标记）
        intermodal = []
        try:
            if flights:
                print(f"   开始构建联运方案，航班数量: {len(flights)}")
                intermodal = build_flight_rail_combos(_norm_city(destination), safe_date, flights, origin_address, _norm_city(origin), destination_address, optimization_type, primary_mode)
                print(f"   联运方案构建完成，返回数量: {len(intermodal)}")
                all_routes.extend(intermodal)
                print(f"   联运方案已添加到总路线中")
            print(f"   联运(严格): {len(intermodal)} 条")
        except Exception as e:
            print(f"❌ 联运构建失败(严格): {e}")
            import traceback
            traceback.print_exc()

        # 默认在时间优先下隐藏疑似中转/超长等待的航班（可通过 include_hidden 显示）
        hidden_count = 0
        if optimization_type == 'time' and not include_hidden:
            filtered = []
            for r in all_routes:
                if r.get('type') == 'flight' and r.get('suspected_transfer'):
                    hidden_count += 1
                    continue
                filtered.append(r)
            all_routes = filtered

        # 综合优先：计算效用
        if optimization_type == 'composite':
            for r in all_routes:
                _compute_utility(r)

        # 折叠：同一航班 + 不同高铁的多条联运，只保留最优一条
        try:
            before = len(all_routes)
            all_routes = _collapse_multimodal_by_flight(all_routes, optimization_type)
            after = len(all_routes)
            if after < before:
                print(f"   折叠联运：{before} -> {after}")
        except Exception as e:
            print(f"折叠联运失败: {e}")

        # 去重：移除重复的方案（直达航班再做一次基于时间与价格的严格去重）
        all_routes = _dedup_direct_flights(all_routes)
        all_routes = _remove_duplicate_routes(all_routes)
        
        all_routes = enhanced_sort_routes(all_routes, optimization_type, origin, destination)
        # 每类折叠显示：限制数量并按类别重新编号
        try:
            all_routes = _limit_and_index_by_category(all_routes)
        except Exception as e:
            print(f"按类别折叠失败: {e}")

        result = {
            'success': True,
            'routes': all_routes,
            'data': all_routes,
            'total': len(all_routes),
            'hidden_count': hidden_count,
            'optimization_type': optimization_type,
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        }
        self.send_json_response(result)

    def handle_rail_options_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        post_data = self.rfile.read(content_length)
        try:
            data = json.loads(post_data.decode('utf-8'))
        except:
            data = {}
        mid_city = data.get('mid_city') or ''
        final_destination = data.get('destination') or ''
        date = data.get('date') or ''
        earliest = data.get('earliest') or ''
        window_minutes = int(data.get('window_minutes') or 120)
        max_options = int(data.get('max_options') or 5)
        
        # 归一化
        mc = _norm_city(mid_city)
        fd = _norm_city(final_destination)
        
        routes = []
        if mc and fd and date:
            try:
                # 如有 earliest 则优先按窗口筛选
                if earliest:
                    try:
                        routes = find_train_routes_with_earliest(mc, fd, date, earliest, window_minutes, max_options)
                    except Exception as e:
                        print(f"/api/rail_options window failed: {e}")
                        routes = []
                
                # 兜底：如果窗口内无结果或未提供 earliest，则取当天全部车次里若干条
                if not routes:
                    all_trains = find_train_routes_strict(mc, fd, date) or []
                    # 优先选择更短时间、更低价格
                    def _key_tr(t):
                        return (t.get('total_time') or 1e9, (t.get('total_price') if t.get('total_price') is not None else (t.get('price') or 1e9)))
                    all_trains.sort(key=_key_tr)
                    routes = all_trains[:max_options]
                    print(f"/api/rail_options fallback used, pick={len(routes)}")
            except Exception as e:
                print(f"/api/rail_options fallback failed: {e}")
                routes = []
        self.send_json_response({ 'success': True, 'options': routes })

    def handle_rail_price_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        date = payload.get('date') or payload.get('departure_date') or ''
        train_no = payload.get('train_no','')
        from_no = payload.get('from_station_no','')
        to_no = payload.get('to_station_no','')
        seat_types = payload.get('seat_types','')
        prefer = payload.get('seat_label','')
        key = f"price:{date}:{train_no}:{from_no}:{to_no}:{seat_types}"
        now_ts = time.time()
        hit = PRICE_CACHE.get(key)
        if hit and now_ts - hit.get('ts',0) < 600:
            pm = hit.get('pm',{})
        else:
            try:
                sess = requests.Session()
                try:
                    sess.headers.update(DEFAULT_HEADERS)
                    sess.headers.update({"X-Requested-With": "XMLHttpRequest"})
                    requests.packages.urllib3.disable_warnings()
                    sess.get("https://kyfw.12306.cn/otn/leftTicket/init", timeout=6, verify=False)
                except Exception:
                    pass
                pm = query_price(
                    session=sess,
                    date=date,
                    train_no=train_no,
                    from_no=from_no,
                    to_no=to_no,
                    seat_types=seat_types,
                )
            except Exception:
                pm = {}
            PRICE_CACHE[key] = {'ts': now_ts, 'pm': pm}
        price = 0
        if prefer and pm:
            try:
                price = int(pm.get(prefer) or 0)
            except Exception:
                price = 0
        resp = {
            'success': True,
            'price_map': pm,
            'seat_label': prefer,
            'seat_price': price,
            'price_source': 'exact' if price > 0 else 'unknown'
        }
        self.send_json_response(resp)

    def handle_ground_options_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        origin = payload.get('origin') or ''
        destination = payload.get('destination') or ''
        prefer = str(payload.get('prefer') or 'time').strip().lower()  # 'time' | 'price'

        def _to_int(v, default=0):
            try:
                if v is None: return default
                s = str(v).strip()
                if not s: return default
                return int(round(float(s)))
            except Exception:
                return default

        options = []
        try:
            gaode_api = get_gaode_api()
            if not gaode_api:
                raise RuntimeError('gaode api not ready')

            # 规范化为坐标
            def _as_lnglat(val: str):
                if val and isinstance(val, str) and ',' in val:
                    try:
                        parts = val.split(',')
                        return float(parts[0]), float(parts[1])
                    except Exception:
                        pass
                info = gaode_api.get_geocode(val) or {}
                loc = info.get('location') or ''
                if loc and ',' in loc:
                    parts = loc.split(',')
                    return float(parts[0]), float(parts[1])
                return None

            o_ll = _as_lnglat(origin)
            d_ll = _as_lnglat(destination)

            # 吸附到就近站点（真实POI）
            snapped = {'origin': None, 'destination': None}
            if o_ll:
                pois = gaode_api.search_nearby_stations(o_ll[0], o_ll[1], radius=800) or []
                if pois:
                    loc = (pois[0].get('location') or '').split(',')
                    if len(loc) == 2:
                        origin = f"{loc[0]},{loc[1]}"
                        snapped['origin'] = {'name': pois[0].get('name',''), 'location': origin}
                else:
                    origin = f"{o_ll[0]},{o_ll[1]}"
            if d_ll:
                pois = gaode_api.search_nearby_stations(d_ll[0], d_ll[1], radius=800) or []
                if pois:
                    loc = (pois[0].get('location') or '').split(',')
                    if len(loc) == 2:
                        destination = f"{loc[0]},{loc[1]}"
                        snapped['destination'] = {'name': pois[0].get('name',''), 'location': destination}
                else:
                    destination = f"{d_ll[0]},{d_ll[1]}"

            # 驾车（含真实tolls）
            driving = gaode_api.get_route_info(origin, destination, 'driving') or {}
            if driving and driving.get('duration') is not None:
                const_taxi = _to_int(driving.get('taxi_cost'))
                options.append({
                    'id': f"driving_{driving.get('duration','')}_{driving.get('distance','')}",
                    'type': 'driving',
                    'display': f"驾车 {driving.get('duration',0)}分钟 · {driving.get('distance',0)}公里",
                    'duration': _to_int(driving.get('duration')),
                    'distance': _to_int(driving.get('distance')),
                    'price': const_taxi if const_taxi > 0 else _to_int(driving.get('tolls')),  # 优先打车预估，否则高速费
                    'tolls': _to_int(driving.get('tolls')),
                    'description': driving.get('description') or ''
                })
                if const_taxi > 0:
                    options.append({
                        'id': f"taxi_{driving.get('duration','')}_{driving.get('distance','')}",
                        'type': 'taxi',
                        'display': f"打车 约¥{const_taxi}",
                        'duration': _to_int(driving.get('duration')),
                        'distance': _to_int(driving.get('distance')),
                        'price': const_taxi,
                        'tolls': 0,
                        'description': '打车预估（高德）'
                    })

            # 公交/地铁（含票价）
            transit = gaode_api.get_route_info(origin, destination, 'transit') or {}
            if transit and transit.get('duration') is not None:
                segs = transit.get('segments') or []
                has_subway = bool(transit.get('has_subway')) or any(seg.get('type')=='subway' for seg in segs)
                opt_type = 'subway' if has_subway else 'transit'
                name = ''
                try:
                    names = [seg.get('name') for seg in segs if seg.get('name')]
                    name = ' / '.join(names[:2]) if names else ''
                except Exception:
                    name = ''
                options.append({
                    'id': f"{opt_type}_{transit.get('duration','')}_{transit.get('distance','')}",
                    'type': opt_type,
                    'display': f"{('地铁' if opt_type=='subway' else '公交')} {transit.get('duration',0)}分钟 · {transit.get('distance',0)}公里",
                    'duration': _to_int(transit.get('duration')),
                    'distance': _to_int(transit.get('distance')),
                    'price': _to_int(transit.get('cost')),  # 公交换乘票价
                    'tolls': 0,
                    'line_name': name,
                    'description': f"{('地铁' if opt_type=='subway' else '公交')}约{_to_int(transit.get('duration'))}分钟"
                })

            # 排序
            if prefer == 'price':
                options.sort(key=lambda o: (o.get('price', 1e9), o.get('duration', 1e9)))
            else:
                options.sort(key=lambda o: (o.get('duration', 1e9), o.get('price', 1e9)))

        except Exception as e:
            print(f"/api/ground_options error: {e}")
            options = []

        self.send_json_response({
            'success': True,
            'options': options
        })

    def handle_poi_request(self):
        """处理POI搜索请求"""
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        
        keyword = payload.get('keyword', '').strip()
        city = payload.get('city', '').strip()
        
        if not keyword:
            self.send_json_response({
                'success': False,
                'message': '缺少搜索关键词',
                'pois': []
            })
            return
        
        pois = []
        try:
            gaode_api = get_gaode_api()
            if gaode_api:
                # 使用高德POI搜索
                search_city = city if city else '全国'
                results = gaode_api.search_poi(keyword, search_city) or []
                
                for poi in results[:10]:  # 限制返回10个结果
                    pois.append({
                        'id': poi.get('id', ''),
                        'name': poi.get('name', ''),
                        'address': poi.get('address', ''),
                        'location': poi.get('location', ''),
                        'type': poi.get('type', ''),
                        'tel': poi.get('tel', ''),
                        'distance': poi.get('distance', 0)
                    })
        except Exception as e:
            print(f"/api/poi 搜索失败: {e}")
            pois = []
        
        self.send_json_response({
            'success': True,
            'pois': pois,
            'keyword': keyword,
            'city': city
        })

    def send_api_status(self):
        status = {
            'success': True,
            'status': 'success',
            'message': '严格模式服务器运行正常',
            'version': 'realtime-1.0.0',
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'features': [
                '严格全真数据（无模拟）',
                '实时12306高铁查询（窗口筛选）',
                '联运动态衔接（规模与时段）',
                '按需展开车次（/api/rail_options）'
            ]
        }
        self.send_json_response(status)

    def handle_options_request(self):
        """处理获取可选选项的请求"""
        content_length = int(self.headers.get('Content-Length', 0))
        post_data = self.rfile.read(content_length)
        try:
            data = json.loads(post_data.decode('utf-8'))
        except:
            data = {}
        
        segment_type = data.get('type')  # 'flight' 或 'train'
        origin = data.get('origin')
        destination = data.get('destination')
        date = data.get('date')
        
        # 归一化与安全日期
        try:
            origin_n = _norm_city(origin or '')
            dest_n = _norm_city(destination or '')
            from datetime import datetime as _dt
            safe_date = date or _dt.now().strftime('%Y-%m-%d')
        except Exception:
            origin_n, dest_n, safe_date = (origin or ''), (destination or ''), (date or '')
        
        options = []
        
        try:
            if segment_type == 'flight':
                # 优先严格真实航班
                flights = get_flight_data_strict(origin_n, dest_n, safe_date)
                # 若无，尝试枢纽优先（但仅保留同城市目的地的结果）
                if not flights:
                    try:
                        hub_flights = get_flight_data_hub_first(origin_n, dest_n, safe_date) or []
                        flights = [f for f in hub_flights if _norm_city(f.get('destination','')) == dest_n]
                    except Exception:
                        flights = []
                # 组装下拉候选
                for flight in flights[:20]:
                    options.append({
                        'id': f"{flight.get('flight_number', '')}_{flight.get('dep_time', '')}",
                        'display': f"{flight.get('airline', '')} {flight.get('flight_number', '')} {flight.get('dep_time', '')}-{flight.get('arr_time', '')}",
                        'price': flight.get('price', 0),
                        'departure_time': flight.get('dep_time', ''),
                        'arrival_time': flight.get('arr_time', ''),
                        'airline': flight.get('airline', ''),
                        'flight_number': flight.get('flight_number', '')
                    })
                print(f"/api/options flight: {origin_n}->{dest_n} {safe_date} count={len(options)}")
            elif segment_type == 'train':
                # 获取高铁选项（严格）
                trains = find_train_routes_strict(origin_n, dest_n, safe_date)
                for train in trains[:20]:
                    details = train.get('details', [{}])[0]
                    options.append({
                        'id': f"{details.get('train_number', '')}_{details.get('departure_time', '')}",
                        'display': f"{details.get('train_number', '')} {details.get('departure_time', '')}-{details.get('arrival_time', '')}",
                        'price': train.get('price', 0),
                        'departure_time': details.get('departure_time', ''),
                        'arrival_time': details.get('arrival_time', ''),
                        'train_number': details.get('train_number', ''),
                        'seat_type': details.get('seat_selected', '')
                    })
                print(f"/api/options train: {origin_n}->{dest_n} {safe_date} count={len(options)}")
        except Exception as e:
            print(f"/api/options 生成失败: {e}")
            options = []
        
        result = {
            'success': True,
            'options': options,
            'type': segment_type
        }
        self.send_json_response(result)

    def send_cities(self):
        # 优先实时从高德获取；失败则回退到后端内置的完整城市列表
        cities = _gaode_get_cities()
        source = 'gaode'
        if not cities:
            try:
                api = get_gaode_api()
                cities = api.get_all_cities() or []
                source = 'fallback'
            except Exception:
                cities = []
        self.send_json_response({'success': True, 'cities': cities, 'source': source})

    def send_json_response(self, data):
        self.send_response(200)
        self.send_header('Content-type', 'application/json; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()
        json_data = json.dumps(data, ensure_ascii=False, indent=2)
        self.wfile.write(json_data.encode('utf-8'))

# 限制每个类别的数量，并给每个类别内的方案重新编号（category_index 从1开始）

def _limit_and_index_by_category(routes: List[Dict], limits: Dict[str, int] = None) -> List[Dict]:
    if limits is None:
        limits = {'直达': 5, '日内到达': 5, '隔日到达': 3, '其他': 3}
    buckets: Dict[str, List[Dict]] = {'直达': [], '日内到达': [], '隔日到达': [], '其他': []}
    for r in routes:
        cat = r.get('category') or '其他'
        if cat not in buckets:
            cat = '其他'
        buckets[cat].append(r)
    final: List[Dict] = []
    for cat in ['直达','日内到达','隔日到达','其他']:
        arr = buckets.get(cat, [])
        limit = int(limits.get(cat, len(arr))) if limits else len(arr)
        arr = arr[:max(0, limit)]
        for idx, item in enumerate(arr, start=1):
            item['category_index'] = idx
        final.extend(arr)
    return final

if __name__ == '__main__':
    PORT = 5100
    print("=" * 60)
    print(" 启动严格版HTTP服务器（liudang_realtime）")
    print("=" * 60)
    print(f"服务地址: http://localhost:{PORT}")
    print(f"前端页面: http://localhost:{PORT}/frontend/index.html")
    print("=" * 60)
    print("按 Ctrl+C 停止服务")
    print("=" * 60)

    try:
        try:
            webbrowser.open(f"http://localhost:{PORT}/frontend/index.html")
        except Exception:
            pass
        with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd:
            httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n服务器已停止")
    except Exception as e:
        print(f"服务器启动失败: {e}") 